/*
 * Copyright 2008 Lukas Vlcek
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 */

package org.radio.lastfm.panel;

import com.googlecode.ascrblr.api.radio.MetaData;
import com.googlecode.ascrblr.api.radio.StationUrl;
import java.awt.Component;
import java.awt.Cursor;
import java.io.Serializable;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JOptionPane;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import org.openide.awt.HtmlBrowser;
import org.openide.util.Lookup;
import org.openide.util.Lookup.Result;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.NbBundle;
import org.openide.util.NbPreferences;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;
import org.openide.util.Utilities;
import org.radio.lastfm.control.PlayerButtonControl;
import org.radio.lastfm.control.action.Play;
import org.radio.lastfm.control.action.Tunein;
import org.radio.lastfm.utils.CallbackAction;
import org.radio.lastfm.utils.ThrowableDelivery;
import org.radio.lastfm.service.PlayerService;

/**
 * Top component which displays something.
 */
final class LastFMPanelTopComponent extends TopComponent implements LookupListener {

    private static final Logger logger = Logger.getLogger(LastFMPanelTopComponent.class.getName());
    
    private final Lookup lookup;
    private final Lookup.Result<MetaData> result;
    private PlayerButtonControl control;
    
    private static LastFMPanelTopComponent instance;
    /** path to the icon used by the component and its open action */
    static final String ICON_PATH = "org/radio/lastfm/icons/favicon.PNG";

    private static String username;
    private static String userpswd;
    
    private static final String PREFERRED_ID = "LastFMPanelTopComponent";

    private LastFMPanelTopComponent() {
        initComponents();
        
        control = new PlayerButtonControl(this.jbPlay, this.jbSkip, this.jbStop, this.jbTuneIn);
        PlayerService.setControl(Tunein.getInstance());
        
        setName(NbBundle.getMessage(LastFMPanelTopComponent.class, "CTL_LastFMPanelTopComponent"));
        setToolTipText(NbBundle.getMessage(LastFMPanelTopComponent.class, "HINT_LastFMPanelTopComponent"));
        setIcon(Utilities.loadImage(ICON_PATH, true));
        
        // this sets style
        jTPMetaData.setText(getHtmlForMetaData(null));
        // clicking links opens browser
        jTPMetaData.addHyperlinkListener(new HyperlinkListener() {
            
            public void hyperlinkUpdate(HyperlinkEvent evt) {
                if (evt.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
                    logger.log(Level.FINE, "Showing URL: " + evt.getURL().toString());
                    HtmlBrowser.URLDisplayer.getDefault().showURL(evt.getURL());
                }
            }
        });
        
        loadUserPreferencies();
        
        lookup = PlayerService.getStaticLookup();
        result = lookup.lookup(new Lookup.Template(MetaData.class));
        result.addLookupListener(this);
        updateMetaData(result);
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        jPanel1 = new javax.swing.JPanel();
        jlUserName = new javax.swing.JLabel();
        jTUserName = new javax.swing.JTextField();
        jlPassword = new javax.swing.JLabel();
        jPUserPassword = new javax.swing.JPasswordField();
        jcTag = new javax.swing.JComboBox();
        jLabel3 = new javax.swing.JLabel();
        jTSearchValue = new javax.swing.JTextField();
        jLabel4 = new javax.swing.JLabel();
        jCRememberMe = new javax.swing.JCheckBox();
        jbTuneIn = new javax.swing.JButton();
        jScrollPane2 = new javax.swing.JScrollPane();
        jTPMetaData = new javax.swing.JTextPane();
        jlUserStatus = new javax.swing.JLabel();
        jbStop = new javax.swing.JButton();
        jbPlay = new javax.swing.JButton();
        jbSkip = new javax.swing.JButton();

        jPanel1.setBorder(javax.swing.BorderFactory.createEtchedBorder());

        org.openide.awt.Mnemonics.setLocalizedText(jlUserName, "User name");

        org.openide.awt.Mnemonics.setLocalizedText(jlPassword, "Password");

        jcTag.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "artist", "globaltags" }));

        org.openide.awt.Mnemonics.setLocalizedText(jLabel3, "Radio station related to");

        jTSearchValue.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jTSearchValueActionPerformed(evt);
            }
        });

        org.openide.awt.Mnemonics.setLocalizedText(jLabel4, "Lookup for");

        org.openide.awt.Mnemonics.setLocalizedText(jCRememberMe, "Remember (excluding password)");
        jCRememberMe.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jCRememberMeActionPerformed(evt);
            }
        });

        org.openide.awt.Mnemonics.setLocalizedText(jbTuneIn, "Tune in");
        jbTuneIn.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jbTuneInActionPerformed(evt);
            }
        });

        org.jdesktop.layout.GroupLayout jPanel1Layout = new org.jdesktop.layout.GroupLayout(jPanel1);
        jPanel1.setLayout(jPanel1Layout);
        jPanel1Layout.setHorizontalGroup(
            jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
            .add(jPanel1Layout.createSequentialGroup()
                .addContainerGap()
                .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                    .add(jPanel1Layout.createSequentialGroup()
                        .add(jLabel3)
                        .add(6, 6, 6)
                        .add(jcTag, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))
                    .add(jCRememberMe)
                    .add(jPanel1Layout.createSequentialGroup()
                        .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                            .add(jlUserName)
                            .add(jlPassword)
                            .add(jLabel4))
                        .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                        .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                            .add(org.jdesktop.layout.GroupLayout.TRAILING, jTUserName, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 297, Short.MAX_VALUE)
                            .add(jPanel1Layout.createSequentialGroup()
                                .add(jTSearchValue, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 224, Short.MAX_VALUE)
                                .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                                .add(jbTuneIn))
                            .add(jPUserPassword, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 297, Short.MAX_VALUE))))
                .addContainerGap())
        );
        jPanel1Layout.setVerticalGroup(
            jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
            .add(jPanel1Layout.createSequentialGroup()
                .addContainerGap()
                .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
                    .add(jlUserName)
                    .add(jTUserName, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
                    .add(jlPassword)
                    .add(jPUserPassword, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
                    .add(jcTag, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                    .add(jLabel3))
                .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                .add(jPanel1Layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
                    .add(jLabel4)
                    .add(jTSearchValue, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                    .add(jbTuneIn))
                .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                .add(jCRememberMe)
                .addContainerGap(org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );

        jTPMetaData.setContentType("text/html");
        jTPMetaData.setEditable(false);
        jTPMetaData.setDoubleBuffered(true);
        jScrollPane2.setViewportView(jTPMetaData);

        org.openide.awt.Mnemonics.setLocalizedText(jlUserStatus, "You must sign in and tune in first...");

        jbStop.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/radio/lastfm/icons/control_stop.png"))); // NOI18N
        org.openide.awt.Mnemonics.setLocalizedText(jbStop, "Stop");
        jbStop.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jbStopActionPerformed(evt);
            }
        });

        jbPlay.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/radio/lastfm/icons/control_play.png"))); // NOI18N
        org.openide.awt.Mnemonics.setLocalizedText(jbPlay, "Play");
        jbPlay.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jbPlayActionPerformed(evt);
            }
        });

        jbSkip.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/radio/lastfm/icons/control_end.png"))); // NOI18N
        org.openide.awt.Mnemonics.setLocalizedText(jbSkip, "Skip");
        jbSkip.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jbSkipActionPerformed(evt);
            }
        });

        org.jdesktop.layout.GroupLayout layout = new org.jdesktop.layout.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
            .add(layout.createSequentialGroup()
                .addContainerGap()
                .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
                    .add(jlUserStatus)
                    .add(layout.createSequentialGroup()
                        .add(jbPlay)
                        .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                        .add(jbSkip)
                        .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                        .add(jbStop)
                        .addContainerGap(155, Short.MAX_VALUE))
                    .add(org.jdesktop.layout.GroupLayout.TRAILING, layout.createSequentialGroup()
                        .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.TRAILING)
                            .add(org.jdesktop.layout.GroupLayout.LEADING, jScrollPane2, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 376, Short.MAX_VALUE)
                            .add(jPanel1, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                        .addContainerGap())))
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING)
            .add(layout.createSequentialGroup()
                .addContainerGap()
                .add(jPanel1, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                .add(jlUserStatus)
                .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE)
                    .add(jbPlay)
                    .add(jbSkip)
                    .add(jbStop))
                .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED)
                .add(jScrollPane2, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 176, Short.MAX_VALUE)
                .addContainerGap())
        );
    }// </editor-fold>//GEN-END:initComponents

private void jbPlayActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jbPlayActionPerformed
    PlayerService.setControl();
    stop(true);
    PlayerService.play(new DeliveryBoy(this)); 
}//GEN-LAST:event_jbPlayActionPerformed

private void jbStopActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jbStopActionPerformed
    PlayerService.setControl();
    stop(true);
    PlayerService.setControl(Play.getInstance(), Tunein.getInstance());
}//GEN-LAST:event_jbStopActionPerformed

private void jbSkipActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jbSkipActionPerformed
    PlayerService.setControl();
    stop(true);
    PlayerService.play(new DeliveryBoy(this));
}//GEN-LAST:event_jbSkipActionPerformed

/**
 * Keeps user provided values except password. It is possible to keep
 * password in MD5 but I don't want to do it now (because ascrblr does not accept MD5).
 * @param username
 * @param password not used now, can be null
 */
private void rememberUserCredentials(String username, String password, String tag, String searchValue) {
    
    if (jCRememberMe.isSelected()) {
        NbPreferences.forModule(LastFMPanelTopComponent.class).put("rememberme", "yes");
        NbPreferences.forModule(LastFMPanelTopComponent.class).put("username", username);
        NbPreferences.forModule(LastFMPanelTopComponent.class).put("tag", tag);
        NbPreferences.forModule(LastFMPanelTopComponent.class).put("search.value", searchValue);
    } else {
        NbPreferences.forModule(LastFMPanelTopComponent.class).remove("rememberme");
        NbPreferences.forModule(LastFMPanelTopComponent.class).remove("username");
        NbPreferences.forModule(LastFMPanelTopComponent.class).remove("tag");
        NbPreferences.forModule(LastFMPanelTopComponent.class).remove("search.value");
    }
}

private void loadUserPreferencies() {
    if ("yes".equals(NbPreferences.forModule(LastFMPanelTopComponent.class).get("rememberme", "false"))) {
        jTUserName.setText(NbPreferences.forModule(LastFMPanelTopComponent.class).get("username", ""));
        jcTag.setSelectedItem(NbPreferences.forModule(LastFMPanelTopComponent.class).get("tag", ""));
        jTSearchValue.setText(NbPreferences.forModule(LastFMPanelTopComponent.class).get("search.value", ""));
        jCRememberMe.setSelected(true);
    } else {
        jCRememberMe.setSelected(false);
    }
}

private void jCRememberMeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jCRememberMeActionPerformed
    rememberUserCredentials(
            jTUserName.getText().trim(),
            jPUserPassword.getText().trim(),
            jcTag.getSelectedItem().toString(),
            jTSearchValue.getText().trim());
}//GEN-LAST:event_jCRememberMeActionPerformed

private void jTSearchValueActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jTSearchValueActionPerformed
}//GEN-LAST:event_jTSearchValueActionPerformed

private void jbTuneInActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jbTuneInActionPerformed
    
    // a few sanity checks first...
    if ("".equals(this.jTUserName.getText().trim())) {
        JOptionPane.showMessageDialog(this, "User name can not be empty!");
        return;
    }
    
    if ("".equals(this.jPUserPassword.getText().trim())) {
        JOptionPane.showMessageDialog(this, "Password can not be empty!");
        return;
    }
    
    if ("".equals(this.jTSearchValue.getText().trim())) {
        JOptionPane.showMessageDialog(this, "Please fill in Lookup field!");
        return;
    }
    
    instance.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    
    // doing radio stuff...
    stop(false);
    PlayerService.setControl();
    
    final String username = this.jTUserName.getText().trim();
    String userpswd = this.jPUserPassword.getText().trim();
    String tag = this.jcTag.getSelectedItem().toString();
    String search = this.jTSearchValue.getText().trim();
    rememberUserCredentials(username, /*userpswd*/ null, tag, search);
    
    StationUrl station = PlayerService.getNewStationUrl(tag,search);
    
    DeliveryBoy delivery = new DeliveryBoy(this);
    
    this.username = username;
    this.userpswd = userpswd;
    
    // thisComponent can be referenced in anonymous inner classes
    final Component thisComponent = instance;
    
    PlayerService.tuneInStation(station, username, userpswd, delivery,
            new CallbackAction() { public void execute() {
                jTUserName.setVisible(false);
                jlUserName.setVisible(false);
                jPUserPassword.setVisible(false);
                jlPassword.setVisible(false);
                jlUserStatus.setText( "User " + username + " is tuned into " +
                        PlayerService.getRadioTagTitle().trim()); 
                thisComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
                PlayerService.setControl(Play.getInstance(), Tunein.getInstance());
                }
            },
            new CallbackAction() { public void execute() {
                jlUserStatus.setText("");
                stop(true);
                PlayerService.setControl(Tunein.getInstance());
                } });
        
}//GEN-LAST:event_jbTuneInActionPerformed

/**
 * Stops PlayerService and sets cursor to DEFAULT_CURSOR
 */
private void stop(boolean mouseCursorToDefault) {
    // thisComponent can be referenced in anonymous inner classes
    if (instance != null) {
    final Component thisComponent = instance;
        if (mouseCursorToDefault) {
            PlayerService.stop(
                    new CallbackAction() { public void execute() {
                        thisComponent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
                        }
                    }
                    );
        }
        else
            PlayerService.stop(null);
    }
}


    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JCheckBox jCRememberMe;
    private javax.swing.JLabel jLabel3;
    private javax.swing.JLabel jLabel4;
    private javax.swing.JPasswordField jPUserPassword;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JScrollPane jScrollPane2;
    private javax.swing.JTextPane jTPMetaData;
    private javax.swing.JTextField jTSearchValue;
    private javax.swing.JTextField jTUserName;
    private javax.swing.JButton jbPlay;
    private javax.swing.JButton jbSkip;
    private javax.swing.JButton jbStop;
    private javax.swing.JButton jbTuneIn;
    private javax.swing.JComboBox jcTag;
    private javax.swing.JLabel jlPassword;
    private javax.swing.JLabel jlUserName;
    private javax.swing.JLabel jlUserStatus;
    // End of variables declaration//GEN-END:variables
    /**
     * Gets default instance. Do not use directly: reserved for *.settings files only,
     * i.e. deserialization routines; otherwise you could get a non-deserialized instance.
     * To obtain the singleton instance, use {@link findInstance}.
     */
    public static synchronized LastFMPanelTopComponent getDefault() {
        if (instance == null) {
            instance = new LastFMPanelTopComponent();
        }
        return instance;
    }

    /**
     * Obtain the LastFMPanelTopComponent instance. Never call {@link #getDefault} directly!
     */
    public static synchronized LastFMPanelTopComponent findInstance() {
        TopComponent win = WindowManager.getDefault().findTopComponent(PREFERRED_ID);
        if (win == null) {
            Logger.getLogger(LastFMPanelTopComponent.class.getName()).warning(
                    "Cannot find " + PREFERRED_ID + " component. It will not be located properly in the window system.");
            return getDefault();
        }
        if (win instanceof LastFMPanelTopComponent) {
            return (LastFMPanelTopComponent) win;
        }
        Logger.getLogger(LastFMPanelTopComponent.class.getName()).warning(
                "There seem to be multiple components with the '" + PREFERRED_ID +
                "' ID. That is a potential source of errors and unexpected behavior.");
        return getDefault();
    }

    @Override
    public int getPersistenceType() {
        return TopComponent.PERSISTENCE_ALWAYS;
    }

    @Override
    public void componentOpened() {
        // TODO add custom code on component opening
    }

    @Override
    public void componentClosed() {
        // TODO add custom code on component closing
    }

    /** replaces this in object stream */
    @Override
    public Object writeReplace() {
        return new ResolvableHelper();
    }

    @Override
    protected String preferredID() {
        return PREFERRED_ID;
    }

    final static class ResolvableHelper implements Serializable {

        private static final long serialVersionUID = 1L;

        public Object readResolve() {
            return LastFMPanelTopComponent.getDefault();
        }
    }

    public void resultChanged(LookupEvent arg0) {
        updateMetaData(result);
    }

    private void updateMetaData(Result<MetaData> result) {

        jTPMetaData.setText("");
        
        if (result.allInstances().size() > 0) {
            StringBuffer d = new StringBuffer();
            Collection<MetaData> metaDataCollection = (Collection<MetaData>) result.allInstances();
            
            MetaData md = metaDataCollection.iterator().next();
            String html = getHtmlForMetaData(md);
            jTPMetaData.setText(html);
        }        
    }
    
    private String getHtmlForMetaData(MetaData md) {

        StringBuffer sb = new StringBuffer();
        sb.append("<html><head><style>" +
            "BODY{margin:5pt;font-family:sans-serif;font-size:12pt;background-color:#939393;color:white;} " +
            ".cover{padding:2pt;border-width:1px;border-style:solid;border-color:#cccccc;margin:0pt;} " +
            ".sec{color:#cccccc;} " +
            "a{text-decoration:none;color:white;} " +
            "a:hover{text-decoration:underline;color:white;} " +
            "b{color:black;} " +
            "p{margin-top:2pt;margin:5pt;} " +
            "</style></head><body>");
        if (md != null) {
            sb.append("<b>Artist:</b> <a href='"+md.getArtist().getArtistUrl()+"'>" + md.getArtist().getArtistName() + "<a/><br>");
            sb.append("<b>Song:</b> <a href='"+md.getTrack().getTrackUrl()+"'>" + md.getTrack().getTrackName() + "</a> <span class='sec'>("+
                        md.getTrack().getTrackDuration()+"&nbsp;sec)</span><br>");
            sb.append("<b>Album:</b> <a href='"+md.getAlbum().getAlbumPageUrl()+"'>" + md.getAlbum().getAlbumName() + "</a><br>");
            sb.append("<p><img class='cover' src='" + md.getAlbum().getCoverSmallUrl() + "'></p>");
        }
        sb.append("</body></html>");
        return sb.toString();
    }
    
    /**
     * Purpose of this class is to deliver an Exception message to
     * the user in case tuneIn method fails (bad user credential).
     */
    private class DeliveryBoy implements ThrowableDelivery {

        private Component component;
        private boolean delivered = false;
        
        public DeliveryBoy(Component c) {
            this.component = c;
        }
        
        public void deliver(Throwable t) {
            delivered = true;
            String msg = t.getMessage();
            // ascrblr is not very exact about what went wrong...
            if ("no such user".equals(msg)) {
                msg = "Invalid user name or password.";
            }
            if (msg == null || "".equals(msg)) {
                msg = "Error message delivered by ascrblr is empty.";
            }
            JOptionPane.showMessageDialog(component, msg);
        }

        public boolean thereWasSomethingToDeliver() {
            return delivered;
        }

        public void nothingToDeliver() {
            throw new UnsupportedOperationException("Not supported yet.");
        }
        
    }
}
